package com.apobates.forum.utils.image;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Random;
import javax.imageio.ImageIO;
import com.apobates.forum.utils.Commons;
import com.apobates.forum.utils.image.OverlayTextElement.TextElementConfig;
import java.util.Optional;
import java.util.stream.Stream;

/**
 * 遮盖盒<br/>
 * 现用于为图片增加水印和为二维码图片增加logo
 *
 * @author xiaofanku
 * @since 20200328
 */
public final class OverlayBox {
    //遮盖元素
    private final OverlayElement element;
    //遮盖配置
    private final OverlayConfig config;
    
    private OverlayBox(OverlayElement element, OverlayConfig config) {
        super();
        this.element = element;
        this.config = config;
    }
    
    /**
     * 保存图片到outputFile文件中
     *
     * @param originalImage 原始图片
     * @param outputFile 遮盖后的图片文件
     * @throws IOException
     * @throws OverlayBoxException
     */
    public void toFile(Path originalImage, Path outputFile) throws IOException, OverlayBoxException {
        BufferedImage image = ImageIO.read(originalImage.toFile());
        saveFile(image, outputFile.toFile());
    }
    
    /**
     * 保存图片到outputFile文件中
     *
     * @param originalImageInputStream 原始图片的输入流
     * @param outputFile 遮盖后的图片文件
     * @throws IOException
     * @throws OverlayBoxException
     */
    public void toFile(InputStream originalImageInputStream, Path outputFile) throws IOException, OverlayBoxException {
        BufferedImage image = ImageIO.read(originalImageInputStream);
        saveFile(image, outputFile.toFile());
    }
    
    /**
     * 保存图片到outputFile文件中
     *
     * @param image 原始图片的缓存
     * @param outputFile 遮盖后的图片文件
     * @throws IOException
     * @throws OverlayBoxException
     */
    public void toFile(BufferedImage image, File outputFile) throws IOException, OverlayBoxException {
        saveFile(image, outputFile);
    }
    
    /**
     * 保存图片到outputStream输出流中,调用前需自行判断遮盖元素可用
     *
     * @param originalImage 原始图片
     * @param outputStream 遮盖后的图片输出流
     * @throws IOException
     */
    public void toStream(File originalImage, OutputStream outputStream) throws IOException {
        BufferedImage image = ImageIO.read(originalImage);
        BufferedImage logoImgBuffer = element.getData();
        toStream(image, logoImgBuffer, outputStream, Commons.getFileExtension(originalImage.getName()));
    }
    
    /**
     * 为原始图片增加水印后保存到outputFile文件
     *
     * @param image 原始图片的缓存
     * @param overlayBuffer 遮盖盒里的数据,需要调用前自行判断,保证其可用
     * @param outputFile 遮盖后的图片文件
     * @throws IOException
     * @throws OverlayBoxException
     */
    public void watermark(BufferedImage image, BufferedImage overlayBuffer, File outputFile) throws IOException, OverlayBoxException {
        // 保存后的图片类型
        String type = Commons.getFileExtension(outputFile.getName());
        try (OutputStream out = new FileOutputStream(outputFile)) { //20200329@AutoCloseable
            toStream(image, overlayBuffer, out, type);
        }
    }
    
    private void toStream(BufferedImage image, BufferedImage overlayElementData, OutputStream outputStream, String outImageType) throws IOException, OverlayBoxException {
        int oeWidth = overlayElementData.getWidth(), oeHeight = overlayElementData.getHeight();
        // 水印与图片的面积是否允许水印
        if (!isAllowOverlay(image.getWidth(), image.getHeight(), oeWidth, oeHeight)) {
            isContinue("图片不适合增加遮盖元素", image, outImageType, outputStream);
            return;
        }
        //获得一个遮盖层
        BufferedImage overlay = getOverley(overlayElementData, config.getBackgroundColor(), config.getBorderColor());
        // determine image type and handle correct transparency
        int imageType = "png".equalsIgnoreCase(outImageType) ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
        BufferedImage imagePaint = new BufferedImage(image.getWidth(), image.getHeight(), imageType);
        // initializes necessary graphic properties
        Graphics2D w = (Graphics2D) imagePaint.getGraphics();
        w.drawImage(image, 0, 0, null);
        // 设置透明度
        AlphaComposite alphaChannel = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, config.getOpacity());
        w.setComposite(alphaChannel);
        // 计算遮盖层在图片的位置(左上角的坐标)
        int[] xyArray = getPosition(overlayElementData, config.getPosition(), image.getWidth(), image.getHeight());
        // xyArray[0] x轴坐标,xyArray[1] y轴坐标
        w.drawImage(overlay, xyArray[0], xyArray[1], null);
        // 保存
        ImageIO.write(imagePaint, outImageType, outputStream);
        w.dispose();
    }
    
    private void saveFile(BufferedImage image, File outputFile) throws IOException, OverlayBoxException {
        // 保存后的图片类型
        String type = Commons.getFileExtension(outputFile.getName());
        // 是否继续
        if (null == element) { //没有实例
            isContinueSaveFile("遮盖图片没有实例或不可用", image, type, outputFile);
            return;
        }
        if (!element.isUsable()) { //文件不存在
            isContinueSaveFile("遮盖图片在物理路径无法访问", image, type, outputFile);
            return;
        }
        BufferedImage overlayElementData = element.getData();
        int oeWidth = overlayElementData.getWidth(), oeHeight = overlayElementData.getHeight();
        // 水印与图片的面积是否允许水印
        if (!isAllowOverlay(image.getWidth(), image.getHeight(), oeWidth, oeHeight)) {
            isContinueSaveFile("图片不适合增加遮盖图片", image, type, outputFile);
            return;
        }
        //
        BufferedImage overlay = getOverley(overlayElementData, config.getBackgroundColor(), config.getBorderColor());
        // determine image type and handle correct transparency
        int imageType = "png".equalsIgnoreCase(type) ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
        BufferedImage imagePaint = new BufferedImage(image.getWidth(), image.getHeight(), imageType);
        // initializes necessary graphic properties
        Graphics2D w = (Graphics2D) imagePaint.getGraphics();
        w.drawImage(image, 0, 0, null);
        // 设置透明度
        AlphaComposite alphaChannel = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, config.getOpacity());
        w.setComposite(alphaChannel);
        // 计算遮盖层在图片的位置(左上角的坐标)
        int[] xyArray = getPosition(overlayElementData, config.getPosition(), image.getWidth(), image.getHeight());
        // xyArray[0] x轴坐标,xyArray[1] y轴坐标
        w.drawImage(overlay, xyArray[0], xyArray[1], null);
        // 保存
        ImageIO.write(imagePaint, type, outputFile);
        w.dispose();
    }
    
    /**
     * 返回遮盖配置
     *
     * @return
     */
    public OverlayConfig getConfig() {
        return this.config;
    }
    
    /**
     * 返回遮盖元素,遮盖盒中的元素
     *
     * @return
     */
    public OverlayElement getOverlayElement() {
        return this.element;
    }
    
    /**
     * 若原始图片不适合遮盖是否继续保存
     *
     * @param exceptionMessage 不继续保存时异常的消息内容
     * @param image 原始图片
     * @param type 遮盖后的图片类型
     * @param outputFile 遮盖后的图片文件
     * @throws IOException
     * @throws OverlayBoxException
     */
    private void isContinueSaveFile(String exceptionMessage, BufferedImage image, String type, File outputFile) throws IOException, OverlayBoxException {
        if (!config.isContinueSave()) {
            throw new OverlayBoxException(exceptionMessage);
        }
        ImageIO.write(image, type, outputFile);
    }
    
    private void isContinue(String exceptionMessage, BufferedImage image, String type, OutputStream outputStream) throws IOException, OverlayBoxException {
        if (!config.isContinueSave()) {
            throw new OverlayBoxException(exceptionMessage);
        }
        ImageIO.write(image, type, outputStream);
    }
    
    /**
     * 是否可以为图片增加遮盖.通过面积判断,主要适用于矩形
     *
     * @param width 图片宽度
     * @param height 图片高度
     * @param waterMarkWidth 遮盖图片的宽度
     * @param waterMarkHeight 遮盖图片的高度
     * @return 可以返回true,反之返回false
     */
    private boolean isAllowOverlay(int width, int height, int overlayWidth, int overlayHeight) {
        //计算图片的面积
        int imgSV = width * height;
        //计算水印的面积
        int wmSV = overlayWidth * overlayHeight;
        if (wmSV >= imgSV) {
            return false; //水印的大小比图片大
        }
        return imgSV / wmSV > Math.PI;
    }
    //获取遮盖层
    private BufferedImage getOverley(BufferedImage logoImgBuffer, BoxColor backgroundColor, BoxColor borderColor) {
        int logoWidth = logoImgBuffer.getWidth(), logoHeight = logoImgBuffer.getHeight();
        
        Image tmp = logoImgBuffer.getScaledInstance(logoWidth, logoHeight, Image.SCALE_SMOOTH);
        BufferedImage resized = new BufferedImage(logoWidth, logoHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = resized.createGraphics();
        //设置背景色
        if (BoxColor.NONE != backgroundColor) {
            Optional<Color> bgc = getOverlayColor(backgroundColor);
            if (bgc.isPresent()) {
                g2d.setColor(bgc.get());
                g2d.fillRect(0, 0, logoWidth, logoHeight);
            }
        }
        g2d.drawImage(tmp, 0, 0, null);
        if (BoxColor.NONE != borderColor) {
            Optional<Color> bc = getOverlayColor(borderColor);
            if (bc.isPresent()) {
                g2d.setColor(bc.get());
                g2d.drawRect(0, 0, logoWidth - 1, logoHeight - 1);
                g2d.drawRect(1, 1, logoWidth - 1, logoHeight - 1);
                g2d.drawRect(0, 0, logoWidth - 2, logoHeight - 2);
            }
        }
        g2d.dispose();
        return resized;
    }
    //计算遮盖颜色(背景,边框)
    private Optional<Color> getOverlayColor(BoxColor color) {
        if (BoxColor.NONE == color) {
            return Optional.empty();
        }
        if (BoxColor.TRANSPARENT == color) {
            return Optional.of(new Color(0f, 0f, 0f, .1f));
        }
        if (BoxColor.WHITE == color) {
            return Optional.of(Color.WHITE);
        }
        if (BoxColor.BLACK == color) {
            return Optional.of(Color.BLACK);
        }
        return Optional.empty();
    }
    //计算遮盖的起码坐标
    private int[] getPosition(BufferedImage logoImgBuffer, BoxPosition position, int imageWidth, int imageHeight) throws IOException {
        //logo的大小
        int logoWidth = logoImgBuffer.getWidth(), logoHeight = logoImgBuffer.getHeight();
        //
        int x = 0, y = 0;
        if (BoxPosition.MIDDLE == position) {
            x = Math.round(imageWidth / 2) - Math.round(logoWidth / 2);
            y = Math.round(imageHeight / 2) - Math.round(logoHeight / 2);
        }
        if (BoxPosition.BOTTOM_RIGHT == position) {
            x = imageWidth - logoWidth;
            y = imageHeight - logoHeight;
        }
        if (BoxPosition.RANDOM == position) {
            Random ra = new Random();
            x = ra.nextInt(imageWidth) + 1;
            y = ra.nextInt(imageHeight) + 1;
            
            //是否越界了
            if (x + logoWidth > imageWidth) {
                x -= imageWidth - logoWidth;
            }
            if (y + logoHeight > imageHeight) {
                y -= imageHeight - logoHeight;
            }
        }
        return new int[]{x, y};
    }
    
    /**
     * 遮盖配置
     *
     * @author xiaofanku
     * @since 20200327
     */
    public static class OverlayConfig {
        private BoxColor backgroundColor = BoxColor.NONE;
        private BoxColor borderColor = BoxColor.NONE;
        private BoxPosition position = BoxPosition.MIDDLE;
        private float opacity = 1.0f;
        private boolean continueSave = false;
        
        /**
         * 返回默认的遮盖配置<br/>
         * 背景色 (BoxColor.NONE/无),<br/>
         * 边框色 (BoxColor.NONE/无),<br/>
         * 位置 (BoxPosition.MIDDLE/水平垂直居中),<br/>
         * 透明度 (1.0f/不透明),<br/>
         * 遮盖图片增加失败时是否继续保存 (false/不继续保存,抛出OverlayBoxException异常)
         *
         * @return
         */
        public static OverlayConfig getDefault() {
            return new OverlayConfig();
        }
        
        private OverlayConfig() {
            super();
        }
        
        /**
         * 遮盖的背景色,默认为:BoxColor.NONE/无
         *
         * @param backgroundColor
         * @return
         */
        public OverlayConfig backgroundColor(BoxColor backgroundColor) {
            this.backgroundColor = backgroundColor;
            return this;
        }
        
        /**
         * 遮盖的边框色,默认为:BoxColor.NONE/无
         *
         * @param borderColor
         * @return
         */
        public OverlayConfig borderColor(BoxColor borderColor) {
            this.borderColor = borderColor;
            return this;
        }
        
        /**
         * 遮盖的位置,默认为:BoxPosition.MIDDLE/水平垂直居中
         *
         * @param position
         * @return
         */
        public OverlayConfig position(BoxPosition position) {
            this.position = position;
            return this;
        }
        
        /**
         * 遮盖的透明度,默认为:1.0f/不透明
         *
         * @param opacity 有效值为0.xf至1.0f区间
         * @return
         */
        public OverlayConfig opacity(float opacity) {
            this.opacity = opacity;
            return this;
        }
        
        /**
         * 若图片不适合增加遮盖是否继续保存,默认为false
         *
         * @param continueSave true继续保存,false抛出异常(OverlayBoxException)中止操作
         * @return
         */
        public OverlayConfig isContinueSave(boolean continueSave) {
            this.continueSave = continueSave;
            return this;
        }
        
        /**
         * 返回一个包含图片的遮盖盒<br/>
         * 设置二维码的logo时若图片面积大于100X100可能导致无法解析<br/>
         * 在调用此方法前须完成配置选项:背景色,边框色,位置,透明度,遮盖失败时是否继续保存<br/>
         *
         * @param overlayImageFile 遮盖图片
         * @return
         */
        public OverlayBox image(File overlayImageFile) {
            return new OverlayBox(new OverlayImageElement(overlayImageFile), this);
        }
        
        /**
         * 返回一个包含文字的遮盖盒<br/>
         * 采用默认的文字配置: 字体:SansSerif,字号:50,粗体:否,颜色:#FF0000<br/>
         * 在调用此方法前须完成配置选项:背景色,边框色,位置,透明度,遮盖失败时是否继续保存<br/>
         *
         * @param content 文字内容
         * @return
         */
        public OverlayBox text(String content) {
            return new OverlayBox(new OverlayTextElement(content), this);
        }
        
        /**
         * 返回一个包含文字的遮盖盒<br/>
         * 在调用此方法前须完成配置选项:背景色,边框色,位置,透明度,遮盖失败时是否继续保存<br/>
         *
         * @param content 文字内容
         * @param textConfig 文字配置
         * @return
         */
        public OverlayBox text(String content, TextElementConfig textConfig) {
            return new OverlayBox(new OverlayTextElement(content, textConfig), this);
        }
        
        public BoxColor getBackgroundColor() {
            return backgroundColor;
        }
        
        public BoxColor getBorderColor() {
            return borderColor;
        }
        
        public BoxPosition getPosition() {
            return position;
        }
        
        public float getOpacity() {
            return opacity;
        }
        
        public boolean isContinueSave() {
            return continueSave;
        }
    }
    
    /**
     * 遮盖的颜色<br/>
     * 适用于背景色和边框;支持:白色(WHITE),黑色(BLACK),透明色(TRANSPARENT),无(NONE)
     *
     * @author xiaofanku
     *
     */
    public enum BoxColor {
        WHITE(1), BLACK(2), TRANSPARENT(3), NONE(4);
        private final int type;
        
        private BoxColor(int type) {
            this.type = type;
        }
        
        public int getType() {
            return type;
        }
    }
    
    /**
     * 遮盖盒的位置<br/>
     * 支持:垂直居中(MIDDLE),右下角(RIGHT_BOTTOM),随机(RANDOM)
     *
     * @author xiaofanku
     * @since 20200327
     */
    public enum BoxPosition {
        MIDDLE(1), BOTTOM_RIGHT(2), RANDOM(3);
        private final int type;
        
        private BoxPosition(int type) {
            this.type = type;
        }
        
        public int getType() {
            return type;
        }
        
        public static BoxPosition getInstance(int type) {
            return Stream.of(BoxPosition.values()).filter(bp->type == bp.getType()).findFirst().orElse(null);
        }
    }
}